package cn.bbstone.pisces2.server.util;

import cn.bbstone.pisces2.comm.Const;
import cn.bbstone.pisces2.proto.rsp.RspFile;
import cn.bbstone.pisces2.proto.rsp.RspFileData;
import cn.bbstone.pisces2.proto.rsp.RspFileFile;
import cn.bbstone.pisces2.server.cache.FileDataCacheItem;
import cn.bbstone.pisces2.server.cache.ServerCache;
import cn.bbstone.pisces2.util.BByteUtil;
import cn.bbstone.pisces2.util.BFileUtil;
import cn.bbstone.pisces2.util.CipherUtil;
import cn.bbstone.pisces2.util.FLIUtil;
import cn.hutool.core.util.IdUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * build client request msg & server response ByteBuf msg
 */
public class MsgUtil {
    private static Logger log = LoggerFactory.getLogger(MsgUtil.class);

    // ---------- build req msg
    public static ByteBuf buildReq(Byte msgType, long fileNo) {
        return buildReq(msgType, fileNo, (short) 0, new byte[0]);
    }

    public static ByteBuf buildReq(Byte msgType, long fileNo, short bodyLen, byte[] bodyData) {
        ByteBuf buf = Unpooled.buffer();
        // magic
        buf.writeBytes(BByteUtil.toBytes(Const.magic));
        // msgType
        buf.writeByte(msgType); // 1B: msgType
        // msgId
        buf.writeBytes(BByteUtil.toBytes(IdUtil.fastSimpleUUID())); // 32B: uuid
        // reqTs
        buf.writeLong(System.currentTimeMillis()); // 8B
        // fileNo
        buf.writeLong(fileNo);
        // body length
        buf.writeShort(bodyLen); // 2B: msg body Len (0-nobody)
        // bodyData
        buf.writeBytes(bodyData);// empty body
        log.debug("{} req ({}) byte[]: {}, ", msgType, buf.readableBytes(), buf.array());
        return buf;
    }

    public static ByteBuf buildRsp(RspFile rspFile) {
        ByteBuf buf = Unpooled.buffer();
        // magic
        buf.writeBytes(BByteUtil.toBytes(Const.magic));
        // msgType
        buf.writeByte(rspFile.getMsgType());
        // msgId
        buf.writeBytes(BByteUtil.toBytes(rspFile.getMsgId()));
        // reqTs
        buf.writeLong(rspFile.getReqTs());
        // rspTs
        buf.writeLong(rspFile.getRspTs());
        // fileNo
        buf.writeLong(rspFile.getFileNo());
        // fileSize
        buf.writeLong(rspFile.getFileSize());
        // chunkNo
        buf.writeInt(rspFile.getChunkNo());
        // chunks
        buf.writeInt(rspFile.getChunks());
        // body checksum
        if (rspFile.getChecksum() == null) { // empty file(no data)
            buf.writeBytes(new byte[32]);
        } else {
            buf.writeBytes(BByteUtil.toBytes(rspFile.getChecksum())); // bodyData's checksum hex to byte[]
        }
        // body length
        buf.writeInt(rspFile.getBodyLen());
        // body data (when transfer fli.idx file(msgType=0x03), body data is this file's checksum), other
        buf.writeBytes(rspFile.getBodyData()); // bodyData is
//        log.info("msgType: {} req ({}) byte[]: {}, ", rspFile.getMsgType(), buf.readableBytes(), buf.array());
        return buf;
    }

    public static void sendFileInfo(ChannelHandlerContext channelHandlerContext, Byte rspMsgType, long fileNo, long reqTs) throws Exception {
        File serverFile = fileNo == 0 ? new File(FLIUtil.getServerIndexPath()) : BFileUtil.newFile(fileNo);
        if (serverFile == null) {
            throw new RuntimeException(String.format("not found server file for fileNo: %d", fileNo));
        }
        log.debug("going to send fileInfo for -> fileNo: {}, serverFile: {}", fileNo, serverFile.getAbsolutePath());
        String fileChecksum = BFileUtil.checksum(serverFile);
        long fileSize = serverFile.length();
        log.debug("fileSize: {}B, file checksum: {}, file: {}", fileSize, fileChecksum, serverFile.getAbsolutePath());
//        if (fileNo > 0) {
            // BUGFIX: will cause OOM
//            String fileBytesHex = BFileUtil.file2BytesHex(serverFile);
//            log.info("==== file.bytes.length: {}, file.bytes: {} ====", fileBytesHex.length(), fileBytesHex);
//        }

        FileInputStream fileInputStream = new FileInputStream(serverFile);

        // slice file which > 4M
        long chunks = (fileSize % Const.DEFAULT_CHUNK_SIZE == 0) ? fileSize / Const.DEFAULT_CHUNK_SIZE
                : (fileSize / Const.DEFAULT_CHUNK_SIZE + 1);
        // TODO when send REQ_FILE_INFO many times, if the file is tranferring data, following 2 data will not update
        // check the fileNo relative file whether in transferring data, if it is, response with realtime info
        int chunkNo = 0;
        int chunkSize = 0;

//        long lines = FLIUtil.getServerIndexLines();

        // send fli.idx file info (fileSize, checksum, etc)
        // build List chunk Rsp
        RspFileFile rspFileInfo = new RspFileFile();
        rspFileInfo.setMsgType(rspMsgType);
        rspFileInfo.setMsgId(IdUtil.fastSimpleUUID());
        rspFileInfo.setReqTs(reqTs);
        rspFileInfo.setRspTs(System.currentTimeMillis());
        rspFileInfo.setFileNo(fileNo); // 0x01(fli.idx) fileInfo.fileNo = lines
        rspFileInfo.setFileSize(fileSize);
        rspFileInfo.setChunkNo(chunkNo);
        rspFileInfo.setChunks((int)chunks);
        rspFileInfo.setBodyLen(32); // md5("fli.idx") has 32 Hex char, used 32B represent
        rspFileInfo.setBodyData(BByteUtil.toBytes(fileChecksum));
        rspFileInfo.setChecksum(CipherUtil.md5(fileChecksum));

        ByteBuf rsp = MsgUtil.buildRsp(rspFileInfo);
        // save info to cache
        // TODO check if the fileNo(relative file) is transferring data, if it is ,donot cover the cache data
        FileDataCacheItem fileDataCacheItem = new FileDataCacheItem();
        fileDataCacheItem.setFileInputStream(fileInputStream);
        fileDataCacheItem.setFileId(fileChecksum);
        fileDataCacheItem.setFileAbsPath(serverFile.getAbsolutePath());
        fileDataCacheItem.setNextReadPos(0);
        fileDataCacheItem.setFileNo(fileNo);
        fileDataCacheItem.setFileSize(fileSize);
        fileDataCacheItem.setChunks((int) chunks);
        fileDataCacheItem.setChunkNo(chunkNo);
        fileDataCacheItem.setChunkSize(chunkSize);
        ServerCache.put(fileNo, fileDataCacheItem);
        // send data
        channelHandlerContext.writeAndFlush(rsp);
    }

    public static void sendFileData(ChannelHandlerContext ctx, Byte rspMsgType, long fileNo, long reqTs) {
        try {
            // TODO remember to close the FileInputStream after all chunks sent
            FileDataCacheItem fileDataCacheItem = ServerCache.get(fileNo);
            // skip to current read location of the fileInputStream
            long nextReadPos = fileDataCacheItem.getNextReadPos();
            FileInputStream fileInputStream = fileDataCacheItem.getFileInputStream();
//            fileInputStream.skip(nextReadPos);
            log.debug("start read server file(fileNo: {}) from FileInputStream, already.read: {}, estimate.remain.available: {}", fileDataCacheItem.getFileNo(), nextReadPos, fileInputStream.available());
            // calculate chunkSize
            long fileSize = fileDataCacheItem.getFileSize();
            int chunkSize = (int) Math.min(Const.DEFAULT_CHUNK_SIZE, (fileSize - nextReadPos));
            log.debug("current pos: {}, will write {} bytes to channel.", nextReadPos, chunkSize);
            // fill payload
            byte[] chunkData = new byte[chunkSize];
            fileInputStream.read(chunkData);
//            log.info("\t chunkData.length: {}, chunkSize: {}, chunkData: {}", chunkData.length, chunkSize, chunkData);
            //
            int chunks = fileDataCacheItem.getChunks();
            // update cache
            int chunkNo = fileDataCacheItem.getChunkNo() + 1;
            fileDataCacheItem.setChunkNo(chunkNo);
            //
            nextReadPos += chunkData.length;
            fileDataCacheItem.setNextReadPos(nextReadPos);
            fileDataCacheItem.setChunkSize(chunkSize);
            // , HexUtil.encodeHexStr(chunkData, true)
            log.debug("nextReadPos: {}, chunks: {}, chunkNo: {}, chunkSize: {}",
                    nextReadPos, chunks, fileDataCacheItem.getChunkNo(), chunkSize);
            log.info("send total progress: {}/{}, file progress: {}/{}, fileSize: {} B, path: {}.",
                    fileNo, ServerCache.getSpCount(), chunkNo, chunks, fileDataCacheItem.getFileSize(), fileDataCacheItem.getFileAbsPath());
            // ----------------------------------------- send file data
            // build List chunk Rsp
            RspFileData rspFileData = new RspFileData();
            rspFileData.setMsgType(rspMsgType);
            rspFileData.setMsgId(IdUtil.fastSimpleUUID());
            rspFileData.setReqTs(reqTs);
            rspFileData.setRspTs(System.currentTimeMillis());
            rspFileData.setFileNo(fileNo);
            rspFileData.setFileSize(0L);// only INFO have value
            rspFileData.setChunkNo(chunkNo);//
            rspFileData.setBodyLen(chunkData.length);
            rspFileData.setBodyData(chunkData);
//            rspFileData.setBodyData(BByteUtil.convertLatin1ToUTF8(chunkData));

            // set body checksum
            String bodyChecksum = CipherUtil.md5(chunkData);
            rspFileData.setChecksum(bodyChecksum);
            // build ByteBuf
            ByteBuf rsp = MsgUtil.buildRsp(rspFileData);
//            log.info("rsp.readableBytes: {}", rsp.readableBytes());
            // send data
            ctx.writeAndFlush(rsp);
        } catch (IOException e) {
            log.error("handle FILE DATA REQ error.", e);
        }
    }

    public static void closeServerFis(FileDataCacheItem cacheItem) {
        FileInputStream fis = cacheItem.getFileInputStream();
        if (fis != null) {
            try {
                fis.close();
                log.debug(">>>>>>>>>>>> successfully close fis for fileNo: {} <<<<<<<<<", cacheItem.getFileNo());
            } catch (IOException e) {
                log.error(String.format("close FileInputStream for fileNo: %d error.", cacheItem.getFileNo()), e);
            }
        }
    }


    public static void main(String[] args) {
//        String uuid = IdUtil.fastSimpleUUID();
//        byte[] uuidBytes = uuid.getBytes(StandardCharsets.UTF_8);
//        System.out.println(uuidBytes.length);
//        System.out.println("fli.idx bodyLen: " + BByteUtil.toBytes(FLIUtil.FLI_IDX).length);
//        reqIdx();


//        log.info("string.md5: {}", CipherUtil.md5("hello,world!"));
//        log.info("file.md5: {}", BFileUtil.checksum(new File("/Users/bbstone/.fli_server/fli.idx")));
//        byte[] bodyBytes = BByteUtil.toBytes(FLIUtil.FLI_IDX);
//        byte[] bytes = BByteUtil.toBytes(CipherUtil.md5(bodyBytes));
//        log.info("bytes.len: {}", bytes.length);
//        // fli.idx
////        reqIdx();
//        log.info("magic.len: {}", BByteUtil.toBytes(Const.magic).length);



    }

}
